﻿#region License
/*
    Copyright (c) 2010, Paweł Hofman (CodeTitans)
    All Rights Reserved.

    Licensed under the Apache License version 2.0.
    For more information please visit:

    http://codetitans.codeplex.com/license
        or
    http://www.apache.org/licenses/


    For latest source code, documentation, samples
    and more information please visit:

    http://codetitans.codeplex.com/
*/
#endregion

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Diagnostics;
using CodeTitans.JSon;
using CodeTitans.Bayeux.Requests;
using CodeTitans.Bayeux.Responses;
using System.Globalization;

namespace CodeTitans.Bayeux
{
    public class BayeuxConnection
    {
        public const int ConcurrentRequests = 1;

        private object _syncObject;
        private HttpConnection _httpConnection;
        private BayeuxConnectionState _state;
        private BayeuxRequest _request;
        private IJSonReader _jsonReader;
        private StringBuilder _writerCache;
        private IJSonWriter _jsonWriter;
        private List<string> _subscribedChannels;

        #region Events

        public event EventHandler<BayeuxConnectionEventArgs> Connected;
        public event EventHandler<BayeuxConnectionEventArgs> ConnectionFailed;
        public event EventHandler<BayeuxConnectionEventArgs> Disconnected;
        public event EventHandler<BayeuxConnectionEventArgs> DataReceived;
        public event EventHandler<BayeuxConnectionEventArgs> DataFailed;
        public event EventHandler<BayeuxConnectionEventArgs> EventReceived;

        #endregion

        /// <summary>
        /// Init constructor.
        /// </summary>
        public BayeuxConnection(string url)
        {
            _syncObject = new object();
            _writerCache = new StringBuilder();
            _jsonWriter = new JSonWriter(_writerCache, true);
            _jsonReader = new JSonReader();
            _state = BayeuxConnectionState.Unconnected;
            _subscribedChannels = new List<string>();

            _httpConnection = CreateNewHttpConnection(url);
            _httpConnection.DataReceived += HttpConnection_DataReceived;
            _httpConnection.DataReceiveFailed += HttpConnection_DataReceiveFailed;
        }

        /// <summary>
        /// Creates new HTTP-connection class.
        /// </summary>
        protected virtual HttpConnection CreateNewHttpConnection(string url)
        {
            HttpConnection connection = new HttpConnection(url);
            connection.ContentType = "application/json";

            return connection;
        }

        void HttpConnection_DataReceived(object sender, HttpConnectionEventArgs e)
        {
            IJSonObject jsonData = null;

            try
            {
                // read JSON data:
                jsonData = _jsonReader.ReadAsJSonObject(e.Data);

                if (DataReceived != null)
                    DataReceived(this, new BayeuxConnectionEventArgs(this, e.StatusCode, e.StatusDescription, e.Data, jsonData, null));
            }
            catch (JSonReaderException ex)
            {
                if (DataFailed != null)
                    DataFailed(this, new BayeuxConnectionEventArgs(this, e.StatusCode, e.StatusDescription, e.Data, null, ex));
            }

            if (jsonData != null)
            {
                int responses = 0;

                // process JSON response, in case it is an array of responses:
                if (jsonData.IsEnumerable)
                {
                    foreach (IJSonObject message in jsonData)
                    {
                        ProcessResponseMessage(e.StatusCode, e.StatusDescription, message);
                        responses++;
                    }
                }
                else
                {
                    ProcessResponseMessage(e.StatusCode, e.StatusDescription, jsonData);
                }

                _request = null;
            }
            else
            {
                Debug.WriteLine("--- No JSON data received from server ---");
            }
        }

        void HttpConnection_DataReceiveFailed(object sender, HttpConnectionEventArgs e)
        {
            _state = BayeuxConnectionState.Unconnected;
            _request.ProcessFailed(e);

            if (ConnectionFailed != null)
                ConnectionFailed(this, new BayeuxConnectionEventArgs(this, e.StatusCode, e.StatusDescription));

            _request = null;
        }

        /// <summary>
        /// Gets the current request that is being processed.
        /// </summary>
        public BayeuxRequest Request
        {
            get { return _request; }
        }

        /// <summary>
        /// Gets or sets the client identifier.
        /// </summary>
        public string ClientID
        { get; set; }

        /// <summary>
        /// Gets the current state of the connection.
        /// </summary>
        public BayeuxConnectionState State
        {
            get { return _state; }
        }

        /// <summary>
        /// Gets or sets the timeout for next requets (ms).
        /// </summary>
        public int Timeout
        {
            get { return _httpConnection.Timeout; }
            set { _httpConnection.Timeout = value; }
        }

        #region Bayeux Handshake

        /// <summary>
        /// Sends handshake request to the server.
        /// </summary>
        public void Handshake(IJSonWritable data, IJSonWritable ext, bool asynchronous)
        {
            if (_state != BayeuxConnectionState.Unconnected)
                throw new InvalidOperationException("Connecting or already connected to bayeux server! Disconnect first");

            _state = BayeuxConnectionState.Connecting;
            SendRequest(new HandshakeRequest(data, ext), asynchronous);
        }

        /// <summary>
        /// Sends handshake request to the server.
        /// </summary>
        public void Handshake()
        {
            Handshake(null, null, true);
        }

        /// <summary>
        /// Sends handshake request to the server in a synchronous way.
        /// Useful only when testing as blocks the current thread.
        /// </summary>
        public void HandshakeSync()
        {
            Handshake(null, null, false);
        }

        #endregion

        #region Bayeux Connect

        /// <summary>
        /// Sends connect request to the server.
        /// </summary>
        public void Connect(IJSonWritable data, IJSonWritable ext, bool asynchronous)
        {
            if (_state != BayeuxConnectionState.Connected)
                throw new InvalidOperationException("Not connected to the server - handshake must be performed first");

            SendRequest(new ConnectRequest(ClientID, BayeuxConnectionTypes.LongPolling, data, ext), asynchronous);
        }

        /// <summary>
        /// Sends connect request to the server.
        /// </summary>
        public void Connect()
        {
            Connect(null, null, true);
        }

        /// <summary>
        /// Sends connect request to the server in a synchronous way.
        /// Useful only when testing as blocks the current thread.
        /// </summary>
        public void ConnectSync()
        {
            Connect(null, null, false);
        }

        #endregion

        #region Bayeux Disconnect

        /// <summary>
        /// Sends disconnect request to the server.
        /// </summary>
        public void Disconnect(IJSonWritable data, IJSonWritable ext, bool asynchronous)
        {
            if (_state != BayeuxConnectionState.Connected)
                throw new InvalidOperationException("Not connected to the server - handshake must be performed first");

            SendRequest(new DisconnectRequest(ClientID, data, ext), asynchronous);
        }

        /// <summary>
        /// Sends disconnect request to the server.
        /// </summary>
        public void Disconnect()
        {
            Disconnect(null, null, true);
        }

        /// <summary>
        /// Sends disconnect request to the server in a synchronous way.
        /// Useful only when testing as blocks the current thread.
        /// </summary>
        public void DisconnectSync()
        {
            Disconnect(null, null, false);
        }

        #endregion

        #region Bayeux Subscribe

        /// <summary>
        /// Sends subscription request to the server.
        /// </summary>
        public void Subscribe(IJSonWritable data, IJSonWritable ext, string channel, bool asynchronous)
        {
            if (_state != BayeuxConnectionState.Connected)
                throw new InvalidOperationException("Not connected to the server - handshake must be performed first");

            if (Subscribed(channel))
                throw new BayeuxException(string.Format(CultureInfo.InvariantCulture, "Already subscribed to channel: '{0}'", channel));

            SendRequest(new SubscribeRequest(ClientID, channel, data, ext), asynchronous);
        }

        /// <summary>
        /// Sends subscription request to the server.
        /// </summary>
        public void Subscribe(string channel)
        {
            Subscribe(null, null, channel, true);
        }

        /// <summary>
        /// Sends subscription request to the server in a synchronous way.
        /// Useful only when testing as blocks the current thread.
        /// </summary>
        public void SubscribeSync(string channel)
        {
            Subscribe(null, null, channel, false);
        }

        #endregion

        #region Bayeux Unsubscribe

        /// <summary>
        /// Sends unsubscription request to the server.
        /// </summary>
        public void Unsubscribe(IJSonWritable data, IJSonWritable ext, string channel, bool asynchronous)
        {
            if (_state != BayeuxConnectionState.Connected)
                throw new InvalidOperationException("Not connected to the server - handshake must be performed first");

            if (!Subscribed(channel))
                throw new BayeuxException(string.Format(CultureInfo.InvariantCulture, "Not subscribed to channel: '{0}'", channel));

            SendRequest(new UnsubscribeRequest(ClientID, channel, data, ext), asynchronous);
        }

        /// <summary>
        /// Sends unsubscription request to the server.
        /// </summary>
        public void Unsubscribe(string channel)
        {
            Unsubscribe(null, null, channel, true);
        }

        /// <summary>
        /// Sends unsubscription request to the server in a synchronous way.
        /// Useful only when testing as blocks the current thread.
        /// </summary>
        public void UnsubscribeSync(string channel)
        {
            Unsubscribe(null, null, channel, false);
        }

        #endregion

        #region Bayeux Publish

        /// <summary>
        /// Sends publish request to the server.
        /// </summary>
        public void Publish(IJSonWritable data, IJSonWritable ext, string channel, IJSonWritable eventData, bool asynchronous)
        {
            if (_state != BayeuxConnectionState.Connected)
                throw new InvalidOperationException("Not connected to the server - handshake must be performed first");

            SendRequest(new PublishRequest(ClientID, channel, eventData, data, ext), asynchronous);
        }

        /// <summary>
        /// Sends subscription request to the server.
        /// </summary>
        public void Publish(string channel, IJSonWritable eventData)
        {
            Publish(null, null, channel, eventData, true);
        }

        /// <summary>
        /// Sends subscription request to the server in a synchronous way.
        /// Useful only when testing as blocks the current thread.
        /// </summary>
        public void PublishSync(string channel, IJSonWritable eventData)
        {
            Publish(null, null, channel, eventData, false);
        }

        #endregion

        [Conditional("DEBUG")]
        public void Send(string message)
        {
            _httpConnection.SendRequest(message, HttpConnection.MethodPost, true);
        }

        public void Send(BayeuxRequest message, bool asynchronous)
        {
            if (_state != BayeuxConnectionState.Connected)
                throw new InvalidOperationException("Not connected");

            SendRequest(message, asynchronous);
        }

        protected void SendRequest(BayeuxRequest message, bool asynchronous)
        {
            if (message == null)
                throw new ArgumentNullException("message");

            lock (_syncObject)
            {
                if (_request != null)
                    _httpConnection.Cancel();

                if (_request != null)
                    throw new InvalidOperationException("Can't start new request, when current has not been finished/cancelled");

                _request = message;

                // serialize JSON data as text message:
                _writerCache.Remove(0, _writerCache.Length);
                message.Write(_jsonWriter);
                var dataToSend = _jsonWriter.ToString();

                _httpConnection.SendRequest(dataToSend, message.RequestMethod, asynchronous);
            }
        }

        public void Cancel()
        {
            if (_httpConnection.IsActive)
                _httpConnection.Cancel();
        }

        /// <summary>
        /// Checks if subscribed to given channel.
        /// </summary>
        public bool Subscribed(string channel)
        {
            return _subscribedChannels.Contains(channel);
        }

        protected virtual void ProcessResponseMessage(HttpStatusCode httpStatusCode, string httpStatusDescription, IJSonObject message)
        {
            string channel = message["channel"].StringValue; // each Bayuex message must have a channel associated!
            BayeuxResponse response = null;

            if (string.IsNullOrEmpty(channel))
                throw new BayeuxException("Unexpected message with empty channel!");
            if (_request == null || (channel.StartsWith("/meta", StringComparison.Ordinal) && channel != _request.Channel))
                throw new BayeuxException(string.Format(CultureInfo.InvariantCulture, "Unexpected response with channel: '{0}'", channel), _request, null, message);
            if (channel.StartsWith("/meta", StringComparison.Ordinal) && _request.ID != message["id"].StringValue)
                throw new BayeuxException(string.Format(CultureInfo.InvariantCulture, "Invalid response ID, current: '{0}', expected: '{1}'", _request.ID, message["id"].StringValue), _request, null, message);

            ///////////////////////////////////////////////////////////////////////////////////////////
            // identify meta messages:
            if (channel == HandshakeRequest.MetaChannel)
            {
                var handshakeResponse = new HandshakeResponse(message);
                response = handshakeResponse;

                // inform, that connection succeeded:
                if (handshakeResponse.Successful)
                {
                    _state = BayeuxConnectionState.Connected;
                    ClientID = response.ClientID;

                    if (string.IsNullOrEmpty(ClientID))
                        throw new BayeuxException("Invalid ClientID received from server", _request, handshakeResponse, message);

                    if (Connected != null)
                        Connected(this, new BayeuxConnectionEventArgs(this, httpStatusCode, httpStatusDescription));
                }
                else
                {
                    _state = BayeuxConnectionState.Unconnected;
                }
            }

            if (channel == DisconnectRequest.MetaChannel)
            {
                response = new BayeuxResponse(message);

                // inform that disconnection succeeded:
                _state = BayeuxConnectionState.Unconnected;
                ClientID = null;
                if (Disconnected != null)
                    Disconnected(this, new BayeuxConnectionEventArgs(this, response.Successful ? HttpStatusCode.OK : HttpStatusCode.BadRequest, null));
            }

            if (channel == SubscribeRequest.MetaChannel)
            {
                SubscribeResponse subscribeResponse = new SubscribeResponse(message);
                response = subscribeResponse;

                if (subscribeResponse.Successful)
                    _subscribedChannels.Add(subscribeResponse.SubscriptionChannel);
            }

            if (channel == UnsubscribeRequest.MetaChannel)
            {
                UnsubscribeResponse unsubscribeResponse = new UnsubscribeResponse(message);
                response = unsubscribeResponse;

                if (unsubscribeResponse.Successful)
                    _subscribedChannels.Remove(unsubscribeResponse.SubscriptionChannel);
            }

            if (_subscribedChannels.Contains(channel))
            {
                response = new BayeuxResponse(message);

                // event from server:
                if (EventReceived != null)
                    EventReceived(this, new BayeuxConnectionEventArgs(this, httpStatusCode, httpStatusDescription, null, message, null));
            }

            // process generic response:
            if (response == null)
            {
                response = new BayeuxResponse(message);
            }

            _request.ProcessResponse(response);
        }
    }
}
